Skip to content

feat(order): orders computed from catalog prices - #6

Merged
EdgarEldy merged 18 commits into
developfrom
feature/order
Sep 19, 2026
Merged

EdgarEldy merged 18 commits into
developfrom
feature/order

Conversation

@EdgarEldy

Copy link
Copy Markdown
Owner

Branch

feature/order

Task checklist

  • core/order: Order entity, OrderDto, OrderRowMapper, OrderMapper, OrderDao + impl, OrderService + impl (depends on the catalog ProductService and the customer CustomerService interfaces only): computes total = quantity * product.unitPrice, checks the referenced customer/product exist
  • core/order's DaoConfig/ServiceConfig (importing the catalog and customer service configs)
  • ws: OrderController, OrderConverter
  • WebMvcConfig imports core/order's config
  • Tests at all five layers, OrderDao with its own fixture dataset, the total computation, the not-found cases for a bad customerId/productId

Commit summary

  • order core: entity, dto, RowMapper, mapper, DAO with optional customer/product filters, service on top of the catalog and customer services (exact totals, quantity bounds, FK race handling), explicit wiring
  • ws: payloads with Bean Validation, converter, controller (list with filters, detail, create; no update/delete), import in WebMvcConfig
  • tests: order module 48, ws 490 (936 across the reactor), including the customer/product deletion refusals once an order exists
  • fixes: a decimal quantity such as 1.5 is refused instead of truncated (Jackson ACCEPT_FLOAT_AS_INT disabled globally), an unreachable check removed
  • README: the order service depends on ProductService and CustomerService only

Test plan

  • Tests exist at every layer the branch's checklist calls for
  • Each DAO test class loads its own dedicated SQL fixture file
  • mvn clean verify is green on the whole reactor

Order is the table-shaped class for the orders table (the class name is Order, the
table is orders because ORDER is an SQL keyword) and OrderDto is the business shape
the service exposes. The quantity is an int and the total a BigDecimal, never a
double, because it is money.

The two shapes stay separate classes.
The RowMapper reads one row into an Order with getInt for the quantity and
getBigDecimal for the total, so the exact decimal survives the round trip. The
entity mapper converts between Order and OrderDto.

Neither queries the database or contains a business rule.
OrderDaoImpl reads a page filtered by an optional customer and an optional
product, counts with the same filters, finds by id and inserts with INSERT ...
RETURNING id. The WHERE clause is built from the filters that are present: only fixed
column names are concatenated and every value is a bound parameter, added in
placeholder order.

There is no update or delete, because orders are immutable in this API.
…stomer services

OrderServiceImpl reaches customers and products only through their service
interfaces, never their DAOs. The injected beans are transactional proxies, so
their calls join the transaction of create, and their ResourceNotFoundException
propagates as a 404. The total is the product price times the quantity, computed
exactly and compared with the column limit before any scaling.

A quantity outside 1 to 1000000 is refused, and a customer or product deleted between
the checks and the insert is reported as not found instead of a 500. The service
returns dto types and pages through PageDto.
DaoConfig declares the DAO, and ServiceConfig imports it together with the catalog
and customer service configurations, by fully qualified name since each module has a
class with that simple name. The order module is therefore self-sufficient: the
ProductService and CustomerService it injects come from those imported configs.

The order service bean is typed by interface so its @transactional proxy applies.
OrderRequest requires the customer, the product and a quantity between 1 and 1000000,
so a missing or absurd value is a 400 before it reaches the service. OrderResponse
exposes the computed total, and OrderConverter maps to and from the core dto and builds
the PageResponse.

Payload classes stay distinct from core dto classes.
OrderController offers the paginated list filterable by customerId and productId,
the detail and the creation, guarded by @PreAuthorize("hasPermission('ORDER',
'READ'|'WRITE')"). It only calls OrderService and answers with an ApiResponse, and a
creation returns 201.

There is no update or delete route, so those methods answer 405.
The order ServiceConfig is added to the WebMvcConfig import list by fully
qualified name. It brings the DAO, and through its own imports the catalog and
customer services it depends on.

With this import every core module is now wired into the single application context.
The order DAO tests run against a real PostgreSQL, so the module declares the
Testcontainers JUnit and PostgreSQL artifacts and the common test-jar holding the
shared singleton container, all with test scope, like the other business modules.
The DAO test loads its own dedicated fixture with customers, products and orders. It
checks page ordering, offset and an out-of-range page, each filter alone and both
together, counts with the same filters, lookups, the exact NUMERIC total round trip
including the column maximum, and the constraints owned by the schema: foreign keys
to the customer and the product and the CHECK on the quantity.
The RowMapper is fed a mocked ResultSet with an int quantity and a BigDecimal total,
and the entity mapper is checked with hand-built objects and no mocks.
…duct services

The order module only sees service interfaces, so the tests mock CustomerService and
ProductService. They cover exact totals (3 x 19.99 = 59.97, a zero price, half-up
rounding), a total above the column limit refused before any insert, the quantity
boundaries, null ids, and the order of the checks: an unknown customer or product
propagates its 404 and nothing is inserted.

A foreign key violation at insert becomes a not-found error, and the page bounds and
filters are checked too.
The real security configuration and a real signed JWT are used with a mocked service.
The tests check status and shape of the three endpoints, validation errors per field,
the customer and product filters passed to the service, page and size violations, an
out-of-range page keeping content as an empty array, the 404 and 422 mappings, and 405
for PUT, DELETE and PATCH. A client-supplied total or id is shown to be ignored.
A parameterized test asserts, for each endpoint, that no token or a revoked one gives
401, that a token holding every other permission or none gives 403, and that a token
holding only the required ORDER permission succeeds.
Customers, categories and products are created through their real endpoints, then the
tests prove that the total is computed from the product price at creation and stays
after the price changes, the filters and pagination, unknown customers and products
giving 404 with nothing stored, and invalid quantities giving 400.

Deleting a customer or a product referenced by an order gives 422 with the exact message,
then succeeds once the order is removed. A decimal quantity such as 1.5 is refused with
400 and stores nothing, which pins the Jackson fix.
By default Jackson accepts a floating point JSON number for an int field and
silently truncates it, so a body with a quantity of 1.5 created an order for one
unit instead of being refused. The shared ObjectMapper now disables
DeserializationFeature.ACCEPT_FLOAT_AS_INT, so such a body is a malformed request and
answers 400 through the existing handler.

An integration test on the order endpoint pins the behaviour.
…ndencies

The three order tasks are checked: the entity, dto, RowMapper, mapper, DAO, service and
configuration of core/order, the controller and converter in ws with the WebMvcConfig
import, and the tests at all five layers with a dedicated fixture for the DAO tests.

The project tree said the order service depends on CategoryService as well, but
computing a total and checking the references only needs ProductService and
CustomerService, so the README now says so.
The exact product is already compared with the column maximum, which has two decimals,
so rounding a value at or below it to two decimals can never exceed it. The second
comparison after setScale and its duplicated message could never fire.

The rounding itself stays, and the unit tests that pin the totals still pass.
@EdgarEldy
EdgarEldy merged commit fb2bbb5 into develop Sep 19, 2026
2 checks passed
@EdgarEldy EdgarEldy mentioned this pull request Sep 19, 2026
8 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant